import traceback
import logging
import time
import os
import threading
import wx
import wx.adv
import datetime
import socket
import argparse

from jobViewer import JobViewer
from server import Server
from login import LdapLogin
from models import Queue, getScriptPath, MyTextBox, loadConfigFile, LOGGING_LEVEL
from statusSender import StatusSender
from jobHandler import JobHandler
from resultSender import ResultSender


# ----------------------------------------------------------------------------------------------------------------------
class CIAgent(wx.adv.TaskBarIcon):
    def __init__(self, logLevel):
        super(CIAgent, self).__init__()
        self._logger = self.createLogger(logLevel)
        self._headerLogInfo = "[" + "TASKBAR_ICON".rjust(19) + "]\t"
        self._configData = loadConfigFile()
        self._isOnline = True
        self._isRunning = True
        self._trayTooltipGreen = 'CI Agent - Enabled'
        self._trayTooltipRed = 'CI Agent - Disabled'
        self._trayIconRed = 'icons/Red.png'
        self._trayIconGreen = 'icons/Green.png'
        self._ip = self.getSelfIP()
        self._port = self._configData['port']
        self._jobList = Queue('JOBS_QUEUE', self._logger)
        self._jobsToSendList = Queue('RESULTS_QUEUE', self._logger)
        self._login = None
        self._svnUsername = "ca_ci_agent"
        self._svnPassword = "agent_pass"
        self._notifsWindowsVisible = True
        self._notifsWindowsShown = False
        self.launch()

    def launch(self):
        self._logger.info(self._headerLogInfo + "Agent started")
        self.setProperIcon()
        self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.createMenu)
        self.Bind(wx.adv.EVT_TASKBAR_RIGHT_DOWN, self.createMenu)
        # self._text = MyTextBox(None, "TESTS RUNNING!!!", 1200, 800, 80)
        self._notifWindow = MyTextBox(None, "")
        self._jobRemovalBox = MyTextBox(None, "")
        self.initThreads()

    def getLoggingParams(self, logLevel):
        if hasattr(self, '_logger') and self._logger is not None:
            self._logger.debug(self._headerLogInfo + "Preparing logging params for logger")
        if logLevel == "SDEBUG":
            loggingFormat = ('[%(asctime)s] - [%(filename)15s] - [%(funcName)23s] - [line: %(lineno)3d]' +
                             ' - [%(levelname)8s] - %(message)s')
            level = logging.DEBUG
        else:
            loggingFormat = '[%(asctime)s] - [%(levelname)8s] - %(message)s'
            level = LOGGING_LEVEL.get(logLevel, logging.INFO)
        if hasattr(self, '_logger') and self._logger is not None:
            self._logger.debug(self._headerLogInfo + "Params for logger: level = {}, loggingFormat = '{}'"
                               .format(logLevel, loggingFormat))
        return loggingFormat, level

    def loggingFileHandler(self, loggingFormat):
        if hasattr(self, '_logger') and self._logger is not None:
            self._logger.debug(self._headerLogInfo + "Creating new file handler for logger: logs/agent_logs.log")
        handler = logging.FileHandler(os.path.join(getScriptPath(), 'logs', 'agent_logs.log'))
        formatter = logging.Formatter(loggingFormat)
        if hasattr(self, '_logger') and self._logger is not None:
            self._logger.debug(self._headerLogInfo + "Setting loggingFormat = '{}' for new handler: logs/agent_logs.log"
                               .format(loggingFormat))
        handler.setFormatter(formatter)
        if hasattr(self, '_logger') and self._logger is not None:
            self._logger.debug(self._headerLogInfo + "Handler logs/agent_logs.log created")
        return handler

    def createLogger(self, logLevel):
        if not os.path.exists('logs'):
            os.makedirs('logs')
        loggingFormat, level = self.getLoggingParams(logLevel)
        logger = logging.getLogger("CI_AGENT")
        logger.setLevel(level)
        logger.addHandler(self.loggingFileHandler(loggingFormat))
        logger.info("-------------------------------------------------------------------------------------------------")
        logger.info("-------------------------------------------------------------------------------------------------")
        return logger

    def getSelfIP(self):
        self._logger.debug(self._headerLogInfo + "Setting IP address for Agent")
        # little hack to be sure its proper ethernet cards IP
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('8.8.8.8', 0))  # connecting to a UDP address doesn't send packets
        return s.getsockname()[0]

    def displayNotifWindows(self):
        if self._notifsWindowsVisible:
            if self._notifsWindowsShown:
                self._notifWindow.Show()
            else:
                self._notifWindow.Hide()
        else:
            self._notifWindow.Hide()

    def changeSwitch(self, param):
        if param:
            return False
        else:
            return True

    def showNotifWindow(self, action="", message=""):
        self.hideNotifWindow()
        if action == "test":
            self._notifWindow.setMessage("TESTS RUNNING!!!", 1200, 200, 80)
        if action == "drop":
            self._notifWindow.setMessage(message, 1200, 200, 30)
        self._notifWindow.Show()
        self._notifsWindowsVisible = True
        self._notifsWindowsShown = True

    def hideNotifWindow(self):
        self._notifWindow.Hide()
        self._notifsWindowsShown = False

    def initThreads(self):
        self._logger.info(self._headerLogInfo + "Initiating all threads")
        # self.createStatusSender()
        self.createJobHandler()
        self.createServer()
        self.createResultSender()
        self._jobViewer = None
        threading.Timer(0.0, self.threadRecovery).start()

    def threadRecovery(self):
        self._logger.debug(self._headerLogInfo + "Threads recovery started")
        count = 0
        while(self._isRunning and count <= 10):
            if not self._server.isAlive():
                self.createServer()
                count += 1
                continue
            # if not self._statusSender.isAlive():
            #     self.createStatusSender()
            #     count += 1
            #     continue
            if not self._jobHandler.isAlive():
                self.createJobHandler()
                count += 1
                continue
            if not self._resultSender.isAlive():
                self.createResultSender()
                count += 1
                continue
            time.sleep(0.5)
            if count > 10:
                self._logger.error(self._headerLogInfo + "Thread recovery stopped because of threads crash flooding!")
            count = 0

    def createServer(self):
        self._server = Server(self._logger, self._ip, self._port, self._jobHandler, self._jobList)

    def createStatusSender(self):
        listOfJobs = self.getListOfJobs()
        self._statusSender = StatusSender(self._logger, self._ip, self._port, self._configData, listOfJobs)

    def createJobHandler(self):
        self._jobHandler = JobHandler(self._logger, self.showNotifWindow, self.hideNotifWindow, self._jobList,
                                      self._jobsToSendList, self._configData, self._svnUsername, self._svnPassword,
                                      None)  # , self._statusSender)

    def createResultSender(self):
        self._resultSender = ResultSender(self._logger, self._jobsToSendList, self._configData, self._svnUsername,
                                          self._svnPassword)

    def getListOfJobs(self):
        self._logger.debug(self._headerLogInfo + "Getting list of existing jobs")
        jobs = []
        for job in self._jobList.getJobsList():
            jobs.append(job._project + "|" + job._branch + "|" + job._testType)
        self._logger.debug(self._headerLogInfo + "Jobs collected: {}".format(jobs))
        return jobs

    def setProperIcon(self):
        self._logger.debug(self._headerLogInfo + "Setting new task bar icon for Agent")
        if self._isOnline:
            self.setIcon(self._trayIconGreen, self._trayTooltipGreen)
            self._logger.debug(self._headerLogInfo + "New task bar icon set: {}".format(self._trayIconGreen))
        else:
            self.setIcon(self._trayIconRed, self._trayTooltipRed)
            self._logger.debug(self._headerLogInfo + "New task bar icon set: {}".format(self._trayIconRed))

    def createMenuItem(self, menu, label, func):
        self._logger.debug(self._headerLogInfo + "Creating menu item: {}, for main popup menu".format(label))
        item = menu.Append(wx.NewId(), label)
        menu.Bind(wx.EVT_MENU, func, item)
        self._logger.debug(self._headerLogInfo + "Menu item: {} added for main popup menu".format(label))
        return item

    def createCheckableMenuItem(self, menu, label, func, checked=False):
        self._logger.debug(self._headerLogInfo + "Creating menu item: {}, for main popup menu".format(label))
        item = menu.AppendCheckItem(wx.NewId(), label)
        menu.Bind(wx.EVT_MENU, func, item)
        if checked:
            item.Check()
        else:
            item.Check(False)
        self._logger.debug(self._headerLogInfo + "Menu item: {} added for main popup menu".format(label))
        return item

    def createMenu(self, event):
        menu = self.createPopupMenu()
        self.PopupMenu(menu)
        menu.Destroy()

    def createPopupMenu(self):
        self._logger.debug(self._headerLogInfo + "Creating main popup menu")
        menu = wx.Menu()
        if self._isOnline:
            self.createCheckableMenuItem(menu, 'Offline', self.onChangeStatus)
        else:
            self.createCheckableMenuItem(menu, 'Online', self.onChangeStatus)
        self.createCheckableMenuItem(menu, 'Jobs', self.onViewJobs)
        menu.AppendSeparator()
        loggingMenu = self.createLoggingMenu()
        menu.Append(wx.ID_ANY, "Set Logging Level", loggingMenu)
        menu.AppendSeparator()
        self.createCheckableMenuItem(menu, 'Notifications', self.onChangeNotifWindow, self._notifsWindowsVisible)
        menu.AppendSeparator()
        self.createCheckableMenuItem(menu, 'Exit', self.onDestroy)
        return menu

    def onChangeNotifWindow(self, event):
        self._notifsWindowsVisible = self.changeSwitch(self._notifsWindowsVisible)
        self.displayNotifWindows()

    def onLoggingMenuPress(self, event):
        menu_id = event.GetId()
        obj = event.GetEventObject()
        menu_label = obj.GetLabelText(menu_id)
        self._logger.debug(self._headerLogInfo + "New logging level chosen from menu: {}".format(menu_label))
        self.setNewLogging(menu_label)

    def createLoggingMenu(self):
        self._logger.debug(self._headerLogInfo + "Creating sub menu: 'Set Logging Level'")
        loggingMenu = wx.Menu()
        loggingLevels = LOGGING_LEVEL.keys()
        for item in loggingLevels:
            self._logger.debug(self._headerLogInfo + "Creating menu item: {}, for submenu: 'Set Logging Level'"
                               .format(item))
            menuItem = wx.MenuItem(loggingMenu, wx.ID_ANY, item)
            loggingMenu.Append(menuItem)
            self.Bind(wx.EVT_MENU, self.onLoggingMenuPress, menuItem)
            self._logger.debug(self._headerLogInfo + "Menu item: {} added for submenu: 'Set Logging Level'"
                               .format(item))
        self._logger.debug(self._headerLogInfo + "Submenu: 'Set Logging Level' created")
        return loggingMenu

    def setNewLogging(self, loggingKey):
        self._logger.debug(self._headerLogInfo + "Setting new logging level: {} for logger".format(loggingKey))
        loggingFormat, level = self.getLoggingParams(loggingKey)
        formatter = logging.Formatter(loggingFormat)
        self._logger.setLevel(level)
        self._logger.debug(self._headerLogInfo + "New level: {} set for logger".format(loggingKey))
        for handler in self._logger.handlers:
            handler.acquire()
            handler.setFormatter(formatter)
            handler.release()
        self._logger.debug(self._headerLogInfo + "New loggingFormat: {} set for logger".format(loggingFormat))

    def onViewJobs(self, event):
        self._logger.debug(self._headerLogInfo + "Job Viewer chosen from menu")
        if self._jobViewer is None:
            self._jobViewer = JobViewer(self._logger,
                                        self,
                                        "Jobs List",
                                        self._jobList,
                                        self._jobHandler,
                                        self._jobsToSendList,
                                        self._configData)
            timer = threading.Timer(0, self.refreshJobs)
            timer.start()
        else:
            self._jobViewer.Show()
            self._jobViewer.SetFocus()

    def refreshJobs(self):
        lastTime = datetime.datetime.now()
        while(self._jobViewer):
            time.sleep(0.2)
            if self._jobViewer is not None:
                lastTimeDifference = datetime.datetime.now() - lastTime
                if (lastTimeDifference > datetime.timedelta(seconds=10) or self._jobList.updateRequired()):
                    lastTime = datetime.datetime.now()
                    self._logger.debug(self._headerLogInfo + "Refreshing Job Viewer list of jobs")
                    self._jobViewer.showJobList()

    def setIcon(self, path, tooltip):
        icon = wx.Icon(path, wx.BITMAP_TYPE_PNG)
        self.SetIcon(icon, tooltip)

    def onChangeStatus(self, event):
        self._logger.debug(self._headerLogInfo + "Trying to change Agent online status")
        if self._login is None:
            self._login = LdapLogin(self.changeCIAgentStatus, self._configData['ldapServer'], self._configData['ldapPort'],
                                    self._logger, self._headerLogInfo)

    def changeCIAgentStatus(self, returnCode, username, text):
        self.destroyLogin()
        if returnCode:
            if self._isOnline:
                self.goOffline(username)
            else:
                self.goOnline(username)
        else:
            if self._isOnline:
                self._logger.error(self._headerLogInfo + "Error while trying to disable CI_Agent "
                                   "(user: " + username + text)
            else:
                self._logger.error(self._headerLogInfo + "Error while trying to enable CI_Agent "
                                   "(user: " + username + text)

    def goOffline(self, username):
        self._logger.info(self._headerLogInfo + "User " + username + " CI_Agent disabled")
        self._isOnline = False
        self.setProperIcon()
        self._server.goOffline()
        self._jobHandler.goOffline()
        # self._statusSender.goOffline()

    def goOnline(self, username):
        self._logger.info(self._headerLogInfo + "User " + username + " CI_Agent enabled")
        self._isOnline = True
        self.setProperIcon()
        self._server.goOnline()
        self._jobHandler.goOnline()
        # self._statusSender.goOnline()

    def destroyViewer(self):
        if self._jobViewer is not None:
            self._jobViewer.Destroy()
            self._jobViewer = None
        self._logger.debug(self._headerLogInfo + "Job Viewer destroyed")

    def destroyLogin(self):
        if self._login is not None:
            self._login.Destroy()
            self._login = None

    def onDestroy(self, event):
        self._isRunning = False
        if self._login is None:
            self._login = LdapLogin(self.destroyCIAgent, self._configData['ldapServer'],
                                    self._configData['ldapPort'], self._logger, self._headerLogInfo)

    def destroyCIAgent(self, returnCode, username, text):
        self._logger.debug(self._headerLogInfo + "Closing Agent")
        self.destroyLogin()
        if returnCode:
            self.destroyAll(username)
        else:
            self._logger.error(self._headerLogInfo + "Error while trying to exit CI_Agent (user: {}). Exception: {}"
                               .format(username, text))

    def destroyAll(self, username):
        self._logger.debug(self._headerLogInfo + "Destroying all threads")
        self._logger.info(self._headerLogInfo + "User " + username + " exited CI_Agent")
        self.destroyViewer()
        self._jobHandler.destroy()
        if self._notifWindow is not None:
            self._notifWindow.Destroy()
        if self._jobRemovalBox is not None:
            self._jobRemovalBox.Destroy()
        self._resultSender.destroy()
        # self._statusSender.destroy()
        time.sleep(0.5)
        self._logger.debug(self._headerLogInfo + "Removing task bar icon")
        self.RemoveIcon()
        self._logger.debug(self._headerLogInfo + "Destroying Agent")
        self.Destroy()


# ----------------------------------------------------------------------------------------------------------------------
def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("-l", dest="loggingLevel", default="DEBUG", type=str,
                        help="optional argument, set Logging level of CI_Agent," +
                        " possible CRITICAL|ERROR|WARNING|INFO|DEBUG|SDEBUG|NOTSET - refer to logging library" +
                        ", default=INFO, SDEBUG - special level with exact point of procedures call",
                        choices=LOGGING_LEVEL.keys())
    return parser.parse_args()


if __name__ == '__main__':
    args = parse_args()
    try:
        app = wx.App()
        CIAgent(args.loggingLevel)
        app.MainLoop()
    except Exception as e:
        print "CRASH:", traceback.print_exc(e)
